22. Exercise: Recording Sleep Quality

L6 43 Navigate SC

Update: In the video above, inside onCreateView() function of SleepTrackerFragment.kt file, the first argument in the sleepTrackerViewModel.navigateToSleepQuality.observe() function would be viewLifecycleOwner instead of the current object this.

L6 44 Record The Sleep Quality SC V2

Update:
In the video above, in the SleepQualityViewModel.kt file, creating own scope uiScope is no longer recommended by Google. The recommended way is to use a lifecycle-aware coroutine scope ViewModelScope, provided by the Architecture components. Therefore, we will not require the viewModelJob object, uiScope coroutine, and the onCleared() function.


As a result, in the onSetSleepQuality() function, launching a coroutine must be done inside a viewModelScope, instead of uiScope.
These updates are similar to what you must have done in SleepTrackerViewModel.kt file previously.

Now it's your turn to complete this exercise yourself.

In this step, you'll add navigation and sleep quality recording to your app.

Navigation

  1. Inspect the provided SleepQualityFragment.kt.

  2. Inspect the provided fragment_sleep_quality.xml.

  3. Open navigation.xml and inspect the code.

    In particular, look for the <argument> where we are passing a sleepNightKey from the SleepTrackerFragment to the SleepQualityFragment.

  4. Open SleepTrackerViewModel.kt.

    You need to add Navigation, so that when the user taps the STOP button, you navigate to the SleepQualityFragment to collect a quality rating.

  5. In SleepTrackerViewModel.kt, in onStopTracking() set a LiveData that changes when you want to navigate.

    Use encapsulation to expose only a gettable version to the fragment:

 private val _navigateToSleepQuality = MutableLiveData<SleepNight>()

 val navigateToSleepQuality: LiveData<SleepNight>
   get() = _navigateToSleepQuality
  1. Add a doneNavigating() function that resets the event.
 fun doneNavigating() {
       _navigateToSleepQuality.value = null
}
  1. In the click handler for the STOP button, onStopTracking(), trigger this navigation:
 _navigateToSleepQuality.value = oldNight
  1. In the SleepTrackerFragment, in onCreateView(), add an observer for navigateToSleepQuality.
sleepTrackerViewModel.navigateToSleepQuality.observe(viewLifecycleOwner, Observer {
})
  1. Inside the observer block, navigate and pass along the ID of the current night, and then call doneNavigating():
night ->
night?.let {
   this.findNavController().navigate(
           SleepTrackerFragmentDirections
                   .actionSleepTrackerFragmentToSleepQualityFragment(night.nightId))
   sleepTrackerViewModel.doneNavigating()
}
  1. Build and run your app. Click START, then click STOP, which should take you to the SleepQualityFragment screen.

    To get back, use the Back button.

Record the Sleep Quality

  1. In the sleepquality package, open SleepQualityViewModel.kt.

  2. Create a SleepQualityViewModel that takes sleepNightKey and database as arguments:

class SleepQualityViewModel(
       private val sleepNightKey: Long = 0L,
       val database: SleepDatabaseDao) : ViewModel() {
}
  1. To navigate back to the SleepTrackerFragment, analogously implement navigateToSleepTracker and _navigateToSleepTracker, as well as doneNavigating():
 private val _navigateToSleepTracker =  MutableLiveData<Boolean?>()

 val navigateToSleepTracker: LiveData<Boolean?>
   get() = _navigateToSleepTracker

 fun doneNavigating() {
        _navigateToSleepTracker.value = null
 }
  1. **Now, create one click handler that you will use for all the smiley sleep quality images, onSetSleepQuality(). **

    Use the same coroutine pattern. Launch a coroutine in uiScope, switch to the IO dispatcher, get tonight using the sleepNightKey, set the sleep quality, update the database, and trigger navigation:

 fun onSetSleepQuality(quality: Int) {
    viewModelScope.launch {
           val tonight = database.get(sleepNightKey) ?: return@withContext
           tonight.sleepQuality = quality
           database.update(tonight)
           _navigateToSleepTracker.value = true
      }
}
  1. Create a SleepQualityViewModelFactory.

    Since this is a version of the same boilerplate code, here is the code. Inspect the code before you move on:

 class SleepQualityViewModelFactory(
       private val sleepNightKey: Long,
       private val dataSource: SleepDatabaseDao) : ViewModelProvider.Factory {

   @Suppress("unchecked_cast")
   override fun <T : ViewModel?> create(modelClass: Class<T>): T {
       if (modelClass.isAssignableFrom(SleepQualityViewModel::class.java)) {
           return SleepQualityViewModel(sleepNightKey, dataSource) as T
       }
       throw IllegalArgumentException("Unknown ViewModel class")
     }
}
  1. Open SleepQualityFragment.

  2. Get the arguments:

val arguments = SleepQualityFragmentArgs.fromBundle(arguments!!)
  1. Get the dataSource:
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  1. And create a factory passing in the dataSource and sleepNightKey:
  val viewModelFactory = SleepQualityViewModelFactory(arguments.sleepNightKey, dataSource)
  1. Get a SleepQualityViewModel reference:
 val sleepQualityViewModel =
       ViewModelProvider(
               this, viewModelFactory).get(SleepQualityViewModel::class.java)
  1. Add the sleepQualityViewModel to the binding object:
binding.sleepQualityViewModel = sleepQualityViewModel
  1. And add the observer, as before:
 sleepQualityViewModel.navigateToSleepTracker.observe(viewLifecycleOwner,  Observer {
   if (it == true) { // Observed state is true.
       this.findNavController().navigate(
               SleepQualityFragmentDirections.actionSleepQualityFragmentToSleepTrackerFragment())
       sleepQualityViewModel.doneNavigating()
     }
})
  1. Open fragment_sleep_quality.xml and add a variable for the SleepQualityViewModel to the <data> block:
   <data>

       <variable
           name="sleepQualityViewModel"
           type="com.example.android.trackmysleepquality.sleepquality.SleepQualityViewModel" />
   </data>
  1. Add a click handler like the one below to each image.

    Match the quality rating to the image.

 android:onClick="@{() -> sleepQualityViewModel.onSetSleepQuality(5)}"
  1. Clear cache, rebuild your app, and make sure it runs without errors.

If you want to start at this step, you can download this exercise from: Step.06-Exercise-Record-SleepQuality.

You will find plenty of //TODO comments to help you complete this exercise, and if you get stuck, go back and watch the video again.

Once you’re done, you can check your solution against the solution we’ve provided here: Step.06-Solution-Record-SleepQuality, or using this git diff.

Task List:

Task Feedback:

Congratulations! You've just built a complete Room database app, using coroutines, and applied just about everything you've learned in the previous lessons!